import {
  DocumentObject,
  IAssemblyConstraint,
  IAssemblyDocTreeNode,
  Types,
} from '@amcax/base';
import {
  PickerAssembly,
  PickerMgr,
  RendererAssembly,
  RendererMgr,
} from '@amcax/renderer';
import { loadProgressEmit } from '@app-base/common/events/CoreEvent';
import { PRODUCT_EVENT } from '@app-cad/common/Constants';
// don't import it from `@app-cad/common/utils`, otherwise TypeError: Cannot read properties of undefined (reading 'workGroup')
//     at new ConstraintIconRenderer (ConstraintIconRenderer.ts:89:1)
import { AssemblySmartMover } from '@app-cad/common/utils/assembly-utils';
import { ctx2, userEvents } from '@app-cad/module';
import {
  addPartToDataManager,
  allParts,
  collectTransformationMatrices,
  getPartDocObjects,
  getPartRefKey,
  getVisualDataAndRenderPart,
  LoadRefDocOptions,
  partMapper,
} from './part-funcs';
import { httpApi } from '@app-base/server';
import { addEntireGeomBox } from './assembly-geomBox-controller';

export let unsortedTreeNodes: IAssemblyDocTreeNode[] = [];

export const refDocNodes = new Map<string, IAssemblyDocTreeNode>();
export const measurementNodes = new Map<string, IAssemblyDocTreeNode>();
export let measurementRootNode: IAssemblyDocTreeNode | undefined;
export let personalWorkspaceDocsInfo: CadDocument.FullDocumentInfo[];

export const subPartsOfRootChildren = new Map<string, Set<string>>();
export const allSubPartKeys = new Set<string>();

let shouldFitView = false;
let hasFittedView = false;

let initialPartsToRender = 0;
let partsToRender = 0;
let currentPartsNumber = 0;
let totalPartsNumber = 0;

const resetPartsNumber = () => {
  currentPartsNumber = 0;
  totalPartsNumber = 0;
};

export function getPartsNumber() {
  return {
    currentPartsNumber,
    totalPartsNumber,
  };
}

export function countTotalPartsNumber(
  docObj: DocumentObject,
  dxids?: string[],
) {
  const { assembly, dxid } = docObj;
  if (assembly?.asmtype === 'part') {
    if (!dxids || dxids.includes(dxid)) {
      totalPartsNumber++;
    }
  }
  if (assembly?.asmtype === 'subassembly' || assembly?.asmtype === 'array') {
    for (const subDoc of docObj.children) {
      countTotalPartsNumber(subDoc, dxids);
    }
  }
}

export function checkAndFitView() {
  if (shouldFitView && !hasFittedView) {
    ctx2.viewer.fitObjectView(400).then(() => {
      hasFittedView = true;
    });
  }
}

export function loadRefDoc(
  docObj: DocumentObject,
  parent: IAssemblyDocTreeNode,
  collectionUUID: string,
  { initialRender = false, noRender = false }: LoadRefDocOptions = {},
): IAssemblyDocTreeNode | undefined {
  try {
    if (docObj.type !== Types.AssemblyComponent) {
      return undefined;
    }
    const { dxid: key, label: title, assembly } = docObj;

    const doc: IAssemblyDocTreeNode = {
      key: key,
      version: parseInt(assembly?.docver, 10),
      collectionUUID,
      title,
      type: assembly?.asmtype === 'subassembly' ? 'product' : assembly?.asmtype,
      parent: parent as IAssemblyDocTreeNode,
      visible: docObj.visibility,
      color: docObj.color,
      children: [],
      assembly,
      originalObj: docObj,
      recursivelyVisible:
        parent.recursivelyVisible ?? (parent.visible && docObj.visibility),
    };
    doc.matrix4Array = collectTransformationMatrices(doc);

    refDocNodes.set(key, doc);

    if (assembly?.asmtype === 'part') {
      if (!noRender) {
        shouldFitView = true;

        if (initialRender) {
          initialPartsToRender++;
        }

        partsToRender++;
      }

      allParts.set(key, doc);

      getPartDocObjects(doc).then((subDocObjs) => {
        doc.children = (subDocObjs.objects || [])
          .filter(({ type }) => type !== 'MeasureObject')
          .map((docObj: DocumentObject) =>
            partMapper(docObj, doc, doc.key, assembly?.params),
          );

        userEvents.emit(PRODUCT_EVENT.ASSEMBLY_TREE_CHANGED);

        addPartToDataManager(doc, getPartRefKey(doc.assembly));

        getVisualDataAndRenderPart(doc, { initialRender, noRender })
          .catch((error) => {
            console.error('Error in loadRefDoc:', error);
          })
          .finally(() => {
            if (!noRender) {
              loadProgressEmit((++currentPartsNumber / totalPartsNumber) * 100);
              partsToRender--;

              if (initialRender) {
                initialPartsToRender--;
                if (initialPartsToRender === 0) {
                  userEvents.emit(PRODUCT_EVENT.ASSEMBLY_RENDERED);
                  resetPartsNumber();
                }
              } else if (partsToRender === 0) {
                if (AssemblySmartMover.smartMoving) {
                  userEvents.emit(PRODUCT_EVENT.SMART_MOVING_RENDERED);
                  // 智能移动时无需 fitObjectView，因此不发送 ASSEMBLY_RENDERED 事件
                } else {
                  userEvents.emit(PRODUCT_EVENT.ASSEMBLY_RENDERED);
                }
                resetPartsNumber();
              }
            }
          });
      });
    } else if (
      assembly.asmtype === 'subassembly' ||
      assembly.asmtype === 'array'
    ) {
      if (assembly.asmtype === 'array') {
        allParts.set(key, doc);
      }

      const subDocs = [];
      for (const subDoc of docObj.children) {
        subDocs.push(
          loadRefDoc(subDoc, doc, collectionUUID, { initialRender, noRender }),
        );
      }

      doc.children = subDocs.filter(Boolean) as IAssemblyDocTreeNode[];
    }

    userEvents.emit(PRODUCT_EVENT.ASSEMBLY_TREE_CHANGED);

    return doc;
  } catch (error) {
    console.error('Error in loadRefDoc:', error);
    return undefined;
  }
}

export async function rebuildConstraintsSubTree(
  docTreeRoot: IAssemblyDocTreeNode,
) {
  let constraints: IAssemblyConstraint[] =
    await ctx2.commandCenters.assemblyCommands.getConstraints();

  unsortedTreeNodes = unsortedTreeNodes.filter(
    (child) => child.type !== 'assembly-constraints-root',
  );

  if (constraints && constraints.length > 0) {
    constraints = constraints.sort((a, b) => a.sortid - b.sortid);

    const constraintNode: IAssemblyDocTreeNode = {
      key: 'assembly-constraints',
      title: '装配约束',
      type: 'assembly-constraints-root',
      parent: docTreeRoot,
      visible: true,
      children: constraints.map((constraint) => ({
        key: constraint.id,
        title: constraint.label,
        type: 'assembly-constraint',
        parent: null,
        visible: true,
        originalObj: constraint,
        recursivelyVisible: true,
      })),
      assembly: null,
      originalObj: null,
      recursivelyVisible: true,
    };

    constraintNode.children.forEach((child) => (child.parent = constraintNode));

    unsortedTreeNodes.push(constraintNode);
  }
}

export function createMeasurementNode(rootObject: DocumentObject) {
  const measurementNode: IAssemblyDocTreeNode = {
    key: rootObject.dxid,
    title: rootObject.label,
    type: 'measurement',
    parent: measurementRootNode,
    visible: rootObject.visibility,
    originalObj: rootObject,
    recursivelyVisible: true,
  };
  return measurementNode;
}

export function addMeasurementNode(
  docTreeRoot: IAssemblyDocTreeNode,
  rootObject: DocumentObject,
) {
  if (!measurementRootNode) {
    measurementRootNode = {
      key: 'measurements',
      title: '测量',
      type: 'measurement-root',
      parent: docTreeRoot,
      visible: true,
      children: [],
      assembly: null,
      originalObj: null,
      recursivelyVisible: true,
    };

    unsortedTreeNodes.push(measurementRootNode);
  }
  const measurementNode = createMeasurementNode(rootObject);
  measurementNodes.set(rootObject.dxid, measurementNode);
  measurementRootNode.children.push(measurementNode);
}

export function applyNodesToTree(docTreeRoot: IAssemblyDocTreeNode) {
  const reservedNodes = docTreeRoot.children.filter((_, index) => index < 4);

  function checkNodeType(typeName: string) {
    return ({ type }: IAssemblyDocTreeNode) => type === typeName;
  }

  const specialNodeTypesInOrder = [
    'assembly-constraints-root',
    'array',
    'measurement-root',
  ];

  const regularNodes = unsortedTreeNodes.filter(
    ({ type }) => !specialNodeTypesInOrder.includes(type),
  );

  docTreeRoot.children = [
    ...reservedNodes,
    ...regularNodes,
    ...specialNodeTypesInOrder.flatMap((typeName) =>
      unsortedTreeNodes.filter(checkNodeType(typeName)),
    ),
  ];

  userEvents.emit(PRODUCT_EVENT.ASSEMBLY_TREE_CHANGED);
}

export async function getDocsInfo(collectionIntID: string) {
  try {
    const {
      data: { personalWorkspace },
    } = await httpApi.getDocuments(collectionIntID);

    personalWorkspaceDocsInfo = personalWorkspace;
  } catch {
    // blank
  }
}

export async function buildAssemblyTree(
  docTreeRoot: IAssemblyDocTreeNode & {
    documentInfo: CadDocument.DocumentInfo;
  },
  { initialRender, noRender }: LoadRefDocOptions,
) {
  try {
    await getDocsInfo(docTreeRoot.documentInfo.collectionId);

    const rootObjects: DocumentObject[] =
      (await ctx2.commandCenters.basicCommands.getObjects())?.objects || [];

    unsortedTreeNodes = [];

    for (const rootObject of rootObjects) {
      countTotalPartsNumber(rootObject);
    }

    for (const rootObject of rootObjects) {
      switch (rootObject.type) {
        case Types.AssemblyComponent:
          {
            const doc = loadRefDoc(
              rootObject,
              docTreeRoot,
              docTreeRoot.collectionUUID,
              { initialRender, noRender },
            );
            if (doc) {
              unsortedTreeNodes.push(doc);
            }
          }
          break;
        case Types.MeasureObject:
          addMeasurementNode(docTreeRoot, rootObject);
          break;
        case Types.GeomBox:
          addEntireGeomBox(rootObject, docTreeRoot);
          break;
        default:
          break;
      }
    }

    await rebuildConstraintsSubTree(docTreeRoot);

    applyNodesToTree(docTreeRoot);

    userEvents.emit(PRODUCT_EVENT.ASSEMBLY_TREE_CHANGED);

    if (!noRender && initialPartsToRender === 0) {
      userEvents.emit(PRODUCT_EVENT.ASSEMBLY_RENDERED);
      resetPartsNumber();
    }
  } catch (error) {
    console.error('Error in buildAssemblyTree:', error);
  }
}

export function gatherSubPartsOfRootChildren(
  docTreeRoot: IAssemblyDocTreeNode,
) {
  subPartsOfRootChildren.clear();

  docTreeRoot.children.forEach((child) => {
    if (['product', 'array'].includes(child.type)) {
      const partKeys = new Set<string>();

      const collectPartKeys = (node: IAssemblyDocTreeNode) => {
        node.children.forEach((subNode) => {
          if (subNode.type === 'part') {
            partKeys.add(subNode.key);
          } else if (['product', 'array'].includes(subNode.type)) {
            collectPartKeys(subNode);
          }
        });
      };

      collectPartKeys(child);

      subPartsOfRootChildren.set(child.key, partKeys);
      partKeys.forEach((key) => allSubPartKeys.add(key));
    }
  });
}

export function removeRenderedRefDoc(refDocNode: IAssemblyDocTreeNode) {
  const picker = PickerMgr.getPicker<PickerAssembly>('assembly');
  picker.clickNull();

  const renderer = RendererMgr.getRender<RendererAssembly>('assembly');
  if (refDocNode.assembly.asmtype === 'part') {
    renderer.deletePart(refDocNode);
  } else {
    renderer.deleteSubAssembly(refDocNode);
  }
}

userEvents.on(PRODUCT_EVENT.REMOVE_DOC, (removedDoc: IAssemblyDocTreeNode) => {
  removeRenderedRefDoc(removedDoc);

  unsortedTreeNodes = unsortedTreeNodes.filter(
    (node) => node.key !== removedDoc.key,
  );

  removedDoc.parent.children = removedDoc.parent.children.filter(
    (child) => child.key !== removedDoc.key,
  );

  if (removedDoc.type === 'part') {
    allParts.delete(removedDoc.key);
  } else {
    const traverseProduct = (doc: IAssemblyDocTreeNode) => {
      doc.children.forEach((child) => {
        if (child.type === 'part') {
          userEvents.emit(PRODUCT_EVENT.REMOVE_DOC, child);
        } else {
          traverseProduct(child);
        }
      });
    };

    traverseProduct(removedDoc);

    if (removedDoc.type === 'array') {
      allParts.delete(removedDoc.key);
    }
  }
});

userEvents.on(
  PRODUCT_EVENT.REMOVE_MEASUREMENT,
  (removedDoc: IAssemblyDocTreeNode) => {
    measurementRootNode.children = measurementRootNode.children.filter(
      (child) => child.key !== removedDoc.key,
    );
    measurementNodes.delete(removedDoc.key);

    if (measurementRootNode.children.length === 0) {
      unsortedTreeNodes = unsortedTreeNodes.filter(
        (node) => node.key !== measurementRootNode.key,
      );

      measurementRootNode.parent.children =
        measurementRootNode.parent.children.filter(
          (child) => child.key !== measurementRootNode.key,
        );

      measurementRootNode = undefined;
    }
  },
);
